﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Windows.Forms;
using CloudShot.Core.Interfaces;
using CloudShot.Entities;
using CloudShot.Interfaces;
using CloudShot.Logic;
using CloudShot.Properties;
using CloudShot.Services;
using CloudShot.Settings;
using CloudShot.Utils;
using CloudShot.Utils.Encoding;

namespace CloudShot.Forms
{
  internal sealed partial class RecordCaptureForm : MoveableForm
  {
    private const int FPS = 10;
    private const int COUNTDOWN = 4;
    private const int DEFAULT_RECORDING_TIME = 10;
    private const int BORDER_SIZE = 8;
    private const int DOUBLE_BORDER_SIZE = BORDER_SIZE * 2;

    private readonly int _recordingTimeInSeconds;

    private readonly Hotkey _followMouseHotkey;
    private readonly Hotkey _recordPauseHotkey;
    private readonly Hotkey _finishRecordingHotkey;
    private readonly IHotkeyService _hotkeyService;

    private readonly Rectangle _captureRect;

    private readonly Timer _timerFollowMouse = new Timer();
    private readonly Timer _timerRecording = new Timer();

    private readonly Stopwatch _stopwatch = new Stopwatch();

    private readonly INotificationService _notificationService;
    private readonly StorageContainer _storageContainer;
    private readonly IConfigurationSection _appConfiguration;

    private float _timeProgress;
    private Rectangle _resizeArea;

    private readonly RecordBackgroundForm _backgroundForm = new RecordBackgroundForm();

    private FastEncodingWorker _worker;
    public Image Image { get; private set; }

    //TODO how do it correctly.
    //TODO so we use correct timer here?
    private bool IsRecording
    {
      get { return _timerRecording.Enabled; }
    }

    private int CooldownTime
    {
      get { return (int)(COUNTDOWN - _stopwatch.Elapsed.TotalSeconds); }
    }

    public RecordCaptureForm(Point location, Size size, IConfigurationSection appConfiguration, INotificationService notificationService,
                             StorageContainer storageContainer)
    {
      _notificationService = notificationService;
      _storageContainer = storageContainer;
      _appConfiguration = appConfiguration;

      _captureRect = new Rectangle(location.X, location.Y, size.Width, size.Height);

      var settings = _appConfiguration.GetOrCreateAs<CloudShotSettings>();
      _recordingTimeInSeconds = settings.RecordingTimeInSeconds == 0 ? DEFAULT_RECORDING_TIME : settings.RecordingTimeInSeconds;

      _followMouseHotkey = settings.FollowMouseHotkey;
      _recordPauseHotkey = settings.StartPauseRecordingHotkey;
      _finishRecordingHotkey = settings.FinishRecordingHotkey;

      _hotkeyService = new HotkeyService(this);

      Icon = Resources.applicationIcon;
      InitializeComponent();

      FormBorderStyle = FormBorderStyle.None;
      DoubleBuffered = true;
      SetStyle(ControlStyles.ResizeRedraw, true);

      lblCapturer.BackColor = TransparencyKey;
      BackColor = TransparencyKey;

      ResetWorker();
    }

    #region Methods

    private void StartRecording()
    {
      _stopwatch.Start();
      _timerRecording.Enabled = true;

      UpdateRecordPauseStatus();
    }

    private void StopRecording()
    {
      _timerRecording.Enabled = false;
      _stopwatch.Reset();
      _timeProgress = 0f;

      UpdateRecordPauseStatus();
    }

    private void PauseRecording()
    {
      _timerRecording.Enabled = false;

      if (CooldownTime < 0)
      {
        _stopwatch.Stop();
      }
      else
      {
        _stopwatch.Reset();
        _timeProgress = 0f;
      }

      UpdateRecordPauseStatus();
    }

    private void RestartCapturing()
    {
      StopRecording();
      ResetWorker();
    }

    private void ResetWorker()
    {
      if (_worker != null)
      {
        _worker.Cancel();
        _worker.FinishEncoding -= OnFinishEncoding;
        _worker = null;
      }

      _worker = new FastEncodingWorker(FPS);
      _worker.FinishEncoding += OnFinishEncoding;
    }

    private void CancelCapturing()
    {
      StopRecording();

      if (_worker != null)
        _worker.Cancel();

      DialogResult = DialogResult.Cancel;
    }

    private void FinishCapturing()
    {
      StopRecording();
      _worker.FinishRecording();
    }

    private void UpdateRecordPauseStatus()
    {
      const string prefix = "  ";

      btnRecPause.Image = _stopwatch.IsRunning ? Resources.record_pause : Resources.record_rec;
      btnRecPause.Enabled = true;

      if (_stopwatch.IsRunning)
      {
        btnRecPause.Text = prefix + Resources.RecordCaptureForm_Pause;
        //toolTip.SetToolTip(btnRecPause, "Pause Recording");
      }
      else if (_timeProgress > 0)
      {
        btnRecPause.Text = prefix + Resources.RecordCaptureForm_Continue;
        //toolTip.SetToolTip(btnRecPause, "Continue Recording");

        _backgroundForm.Label = Resources.RecordCaptureForm_Status_Paused;
        _backgroundForm.Visible = true && Visible;
      }
      else
      {
        btnRecPause.Text = prefix + Resources.RecordCaptureForm_Record;
        //toolTip.SetToolTip(btnRecPause, "Start Recording");

        btnFinishRecord.Enabled = false;
        btnRestartRecord.Enabled = false;

        _backgroundForm.Label = Resources.RecordCaptureForm_Status_Idle;
        _backgroundForm.Visible = true && Visible;
        pnlButtons.Invalidate(false);
      }
    }

    private Bitmap CaptureOneFrame()
    {
      var rect = lblCapturer.RectangleToScreen(lblCapturer.ClientRectangle);

      Bitmap image = ScreenCapture.CaptureRectangle(rect);

      using (Graphics graphics = Graphics.FromImage(image))
      {
        graphics.DrawCurrentCursor(rect.Location);
      }

      return image;
    }

    private void RegisterHotkeys()
    {
      //TODO: almost copy & paste from MainForm.SetupHotkeys
      var errors = new List<string>();
      string error;

      if (!RegisterHotkey(_followMouseHotkey, OnToggleMouseFollowing, chkFollowMouse, Resources.RecordCaptureForm_FollowMouseTooltip, out error))
        errors.Add(error);

      if (!RegisterHotkey(_recordPauseHotkey, OnRecPauseClick, btnRecPause, Resources.RecordCaptureForm_RecordTooltip, out error))
        errors.Add(error);

      if (!RegisterHotkey(_finishRecordingHotkey, OnFinishClick, btnFinishRecord, Resources.RecordCaptureForm_FinishRecordingTooltip, out error))
        errors.Add(error);

      if (errors.Count > 0)
      {
        using (var dialog = new HotkeyOccupiedDialog(errors, SystemIcons.Exclamation, showSettingsButton: false))
        {
          dialog.ShowDialog();  
        }
      }
    }

    private bool RegisterHotkey(Hotkey hotkey, EventHandler callback, Control control, string text, out string error)
    {
      //TODO: almost copy & paste from MainForm.RegisterHotkey
      error = string.Empty;

      if (hotkey.IsEmpty)
      {
        toolTip.SetToolTip(control, text);
        return true;
      }
      
      if (_hotkeyService.RegisterHotkey(hotkey, callback))
      {
        toolTip.SetToolTip(control, string.Format("{0} ({1})", text, hotkey.ToString()));
        return true;
      }

      error = string.Format("{0}: {1}", text, hotkey.ToString());
      return false;
    }

    #endregion

    #region Overridings

    protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
    {
      // This method needed because if set AcceptButton = btnOk and CancelButton = bntCancel, 
      // when press ENTER somehow will be called OnCancelClick.

      if (keyData == Keys.Escape)
      {
        btnCancel.PerformClick();
        return true;
      }

      if (keyData == Keys.Enter)
      {
        btnFinishRecord.PerformClick();
        return true;
      }

      if (keyData == Keys.Space)
      {
        btnRecPause.PerformClick();
        return true;
      }

      return base.ProcessCmdKey(ref msg, keyData);
    }

    #endregion

    #region Event Handlers

    private void OnLoad(object sender, EventArgs e)
    {
      const int pnlButtonsOffset = 14;

      // Necessary for ProcessCmdKey
      KeyPreview = true;

      //To drag form by touching panel
      DragControl = moveHandler;

      // Set position and size of lblCapturer
      lblCapturer.Location = new Point(BORDER_SIZE, BORDER_SIZE);
      lblCapturer.Size = new Size(Width - DOUBLE_BORDER_SIZE, Height - DOUBLE_BORDER_SIZE - pnlButtons.Height - pnlButtonsOffset);
      lblCapturer.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top;

      User32.SetForegroundWindow(Handle);

      // Calculate position of the form
      int x = _captureRect.X - BORDER_SIZE;
      int y = _captureRect.Y - BORDER_SIZE;
      int width = _captureRect.Width + Width - lblCapturer.Width;
      int height = _captureRect.Height + Height - lblCapturer.Height;

      // Set position of the form
      Location = new Point(x, y);
      Size = new Size(width, height);

      // Setup timer
      _timerRecording.Interval = 1000 / FPS; //ms
      _timerRecording.Tick += OnTimerRecordingTick;

      _timerFollowMouse.Interval = 30;
      _timerFollowMouse.Tick += OnTimerFollowMouseOnTick;

      _backgroundForm.Location = PointToScreen(lblCapturer.Location);
      _backgroundForm.Size = lblCapturer.Size;
      _backgroundForm.TopMost = true;

      _backgroundForm.LocationChanged += (o, args) =>
      {
        //set new location of current form, while moving background form
        Location = new Point(_backgroundForm.Location.X - lblCapturer.Left, _backgroundForm.Location.Y - lblCapturer.Top);
      };

      RegisterHotkeys();
      UpdateRecordPauseStatus();
    }

    private void OnFinishClick(object sender, EventArgs e)
    {
      if (!btnFinishRecord.Enabled)//can be called by hotkey
        return;

      FinishCapturing();
    }

    private void OnRecPauseClick(object sender, EventArgs e)
    {
      if (_stopwatch.IsRunning)
        PauseRecording();
      else
        StartRecording();

      UpdateRecordPauseStatus();
    }

    private void OnRestartRecordClick(object sender, EventArgs e)
    {
      RestartCapturing();
    }

    private void OnCancelClick(object sender, EventArgs e)
    {
      CancelCapturing();
    }

    private void OnButtonMouseEnter(object sender, EventArgs e)
    {
      var button = (Button)sender;

      if (button == btnRecPause)
        button.Image = _stopwatch.IsRunning ? Resources.record_pause : Resources.record_rec;
    }

    private void OnButtonMouseLeave(object sender, EventArgs e)
    {
      var button = (Button)sender;

      if (button == btnRecPause)
        button.Image = _stopwatch.IsRunning ? Resources.record_pause : Resources.record_rec;
    }

    private void OnToggleMouseFollowing(object sender, EventArgs e)
    {
      chkFollowMouse.Checked = !chkFollowMouse.Checked;
    }

    private void OnMouseFollowingCheckedChanged(object sender, EventArgs e)
    {
      _timerFollowMouse.Enabled = chkFollowMouse.Checked;
    }

    private void OnCapturerResize(object sender, EventArgs e)
    {
      var rect = lblCapturer.Bounds;
      rect.Inflate(BORDER_SIZE, BORDER_SIZE);

      _resizeArea = rect;
    }

    private void OnFormPaint(object sender, PaintEventArgs e)
    {
      //const float dashSizeInPixels = 8.0F / BORDER_SIZE;

      Graphics graphics = e.Graphics;

      using (var borderPen = new Pen(Color.FromArgb(120, 120, 120), 1))
      using (var backPen = new Pen(Color.Black, BORDER_SIZE))
      using (var dashPen = new Pen(Color.White, BORDER_SIZE - 5))
      {
        dashPen.DashPattern = new[]
        {
          1f,
          1f
        };

        var rect = _resizeArea;
        rect.Inflate(-BORDER_SIZE / 2, -BORDER_SIZE / 2);
        graphics.DrawRectangle(backPen, rect);

        const int offset = 0;
        graphics.DrawLine(dashPen, rect.Left, rect.Top + offset, rect.Left, rect.Bottom - offset);
        graphics.DrawLine(dashPen, rect.Right-1, rect.Top + offset, rect.Right-1, rect.Bottom - offset);

        var borderRect = _resizeArea;
        borderRect.Width -= 1;
        graphics.DrawRectangle(borderPen, borderRect);
      }
    }

    private void OnToolbarPanelPaint(object sender, PaintEventArgs e)
    {
      var control = sender as Control;

      if (control == null)
        return;

      const int progressPenWidth = 5;

      var rect = control.ClientRectangle;
      var graphics = e.Graphics;

      using (var progressBg = new Pen(Color.FromArgb(196, 196, 196), progressPenWidth))
      using (var progressPen = new Pen(Color.Red, progressPenWidth))
      {
        graphics.DrawLine(progressBg, 0, progressPenWidth / 2, rect.Width, progressPenWidth / 2);

        float width = rect.Width * _timeProgress / 100f;
        graphics.DrawLine(progressPen, 0, progressPenWidth / 2, width, progressPenWidth / 2);
      }
    }

    private void OnFinishEncoding(Image image)
    {
      if (image == null)
      {
        ResetWorker();
        return;
      }

      using (var recordPreviewForm = new RecordPreviewForm(image, _storageContainer, _notificationService, _appConfiguration))
      {
        recordPreviewForm.Location = Location;

        Hide();

        var result = recordPreviewForm.ShowDialog();

        if (result == DialogResult.Retry)
        {
          Show();
          RestartCapturing();
        }
        else
        {
          Image = image;
          DialogResult = result;
        } 
      }
    }

    protected override void OnResize(EventArgs e)
    {
      base.OnResize(e);

      _backgroundForm.Size = lblCapturer.Size;
    }

    protected override void OnLocationChanged(EventArgs e)
    {
      base.OnLocationChanged(e);

      _backgroundForm.Location = PointToScreen(lblCapturer.Location);
    }

    protected override void OnClosing(CancelEventArgs e)
    {
      base.OnClosing(e);
      _backgroundForm.Close();
    }

    protected override void OnVisibleChanged(EventArgs e)
    {
      base.OnVisibleChanged(e);
      _backgroundForm.Visible = Visible;
    }

    #endregion

    #region Time Events

    private void OnTimerFollowMouseOnTick(object sender, EventArgs args)
    {
      if (!IsRecording)
        return;

      const float part = (float)2 / 3;

      int width = lblCapturer.Width;
      int height = lblCapturer.Height;

      var rect = new Rectangle
      {
        Height = (int)(height * part),
        Width = (int)(width * part),
        X = (int)(Location.X + width * (1 - part) / 2),
        Y = (int)(Location.Y + height * (1 - part) / 2),
      };

      int xPos = Math.Min(rect.Right, Math.Max(Cursor.Position.X, rect.Left));
      int yPos = Math.Min(rect.Bottom, Math.Max(Cursor.Position.Y, rect.Top));

      Location = new Point(Location.X + (Cursor.Position.X - xPos), Location.Y + (Cursor.Position.Y - yPos));
    }

    private void OnTimerRecordingTick(object sender, EventArgs eventArgs)
    {
      btnRecPause.Enabled = CooldownTime <= 0;

      if (CooldownTime <= 0)
      {
        _backgroundForm.Visible = false;
      }
      else if (CooldownTime > 0)
      {
        _backgroundForm.Label = string.Format(Resources.RecordCaptureForm_CaptureStartsNotification, CooldownTime);
      }

      pnlButtons.Invalidate(false); // update timer progress visualization

      btnFinishRecord.Enabled = _worker.FramesCount > 0;
      btnRestartRecord.Enabled = _worker.FramesCount > 0;

      //after all UI updates, add frame to GIF image
      if (CooldownTime <= 0)
      {
        Bitmap capturedImage = CaptureOneFrame();
        _worker.AddImage(capturedImage);

        float elapsedSeconds = (float)_stopwatch.Elapsed.TotalSeconds - COUNTDOWN;
        _timeProgress = elapsedSeconds * 100f / _recordingTimeInSeconds;

        if (elapsedSeconds >= _recordingTimeInSeconds)
          FinishCapturing();
      }
    }

    #endregion

    #region Resize Capture Window

    protected override void WndProc(ref Message m)
    {
      const UInt32 WM_NCHITTEST = 0x0084;
      const UInt32 WM_MOUSEMOVE = 0x0200;

      const UInt32 HTLEFT = 10;
      const UInt32 HTRIGHT = 11;
      const UInt32 HTBOTTOMRIGHT = 17;
      const UInt32 HTBOTTOM = 15;
      const UInt32 HTBOTTOMLEFT = 16;
      const UInt32 HTTOP = 12;
      const UInt32 HTTOPLEFT = 13;
      const UInt32 HTTOPRIGHT = 14;

      const int RESIZE_HANDLE_SIZE = BORDER_SIZE;

      // Can't resize window after the record was started.
      if (_stopwatch.Elapsed.TotalMilliseconds > 0)
      {
        base.WndProc(ref m);
        return;
      }

      bool handled = false;
      if (m.Msg == WM_NCHITTEST || m.Msg == WM_MOUSEMOVE)
      {
        Point formLocation = _resizeArea.Location;
        Size formSize = _resizeArea.Size;
        Point screenPoint = new Point(m.LParam.ToInt32());
        Point clientPoint = PointToClient(screenPoint);

        var boxes = new Dictionary<UInt32, Rectangle>
        {
          {HTBOTTOMLEFT, new Rectangle(0, formSize.Height - RESIZE_HANDLE_SIZE, RESIZE_HANDLE_SIZE, RESIZE_HANDLE_SIZE)},
          {HTBOTTOM, new Rectangle(RESIZE_HANDLE_SIZE, formSize.Height - RESIZE_HANDLE_SIZE, formSize.Width - 2 * RESIZE_HANDLE_SIZE, RESIZE_HANDLE_SIZE)},
          {HTBOTTOMRIGHT, new Rectangle(formSize.Width - RESIZE_HANDLE_SIZE, formSize.Height - RESIZE_HANDLE_SIZE, RESIZE_HANDLE_SIZE, RESIZE_HANDLE_SIZE)},
          {HTRIGHT, new Rectangle(formSize.Width - RESIZE_HANDLE_SIZE, RESIZE_HANDLE_SIZE, RESIZE_HANDLE_SIZE, formSize.Height - 2 * RESIZE_HANDLE_SIZE)},
          {HTTOPRIGHT, new Rectangle(formSize.Width - RESIZE_HANDLE_SIZE, 0, RESIZE_HANDLE_SIZE, RESIZE_HANDLE_SIZE)},
          {HTTOP, new Rectangle(RESIZE_HANDLE_SIZE, 0, formSize.Width - 2 * RESIZE_HANDLE_SIZE, RESIZE_HANDLE_SIZE)},
          {HTTOPLEFT, new Rectangle(0, 0, RESIZE_HANDLE_SIZE, RESIZE_HANDLE_SIZE)},
          {HTLEFT, new Rectangle(0, RESIZE_HANDLE_SIZE, RESIZE_HANDLE_SIZE, formSize.Height - 2 * RESIZE_HANDLE_SIZE)}
        };

        foreach (KeyValuePair<UInt32, Rectangle> hitBox in boxes)
        {
          var rect = hitBox.Value;
          rect.Offset(formLocation);

          if (rect.Contains(clientPoint))
          {
            m.Result = (IntPtr)hitBox.Key;
            handled = true;
            break;
          }
        }
      }

      if (!handled)
        base.WndProc(ref m);
    }

    #endregion
  }
}